Building a Custom Matching Pairs Component in .NET MAUI

Custom Matching Pairs Component in .NET9 MAUI

In this post, we’ll walk through the design and implementation of a flexible, reusable Matching Pairs component for .NET MAUI. This journey was driven by real-world requirements. Iterative improvements contributed as well. The result is a highly customizable and MVVM-friendly UI control. It is suitable for educational games, language learning, and more.

The full source code is available on GitHub.

The Goal

We wanted a component that:

  • Displays two columns of selectable items (e.g., words or phrases).
  • Lets the user select one item from each column to attempt a match.
  • Provides immediate visual feedback (color changes) for correct and incorrect matches.
  • Disables matched pairs and tracks the number of attempts.
  • Exposes a customizable “Continue” button, only enabled when all pairs are matched.
  • Is highly customizable via bindable properties for colors, styles, text, and behavior.
  • Supports both MVVM commands and event handlers for button actions.

Step 1: The Data Model

We started with a simple MatchingPair class, then extended it to support UI state and property change notifications:

public class MatchingPair : INotifyPropertyChanged
{
    public string Left { get; set; }
    public string Right { get; set; } 
// ... properties for IsMatched, IsLeftEnabled, IsRightEnabled, LeftColor, RightColor, etc. // Implements INotifyPropertyChanged for UI updates }
}

Each item is rendered with a Border and a Label, with colors and enabled state bound to the model.

Step 2: The UI Layout

The component’s XAML uses a Grid with two CollectionViews for the left and right columns, and a Button for confirmation:

<components:MatchingPairsView
	ButtonConfirmCommand="{Binding OnContinueCommand}"
	ButtonConfirmText="Next"
	CorrectColor="LimeGreen"
	DefaultBackgroundColor="White"
	DefaultStrokeColor="Gray"
	DefaultTextColor="Black"
	Delay="300"
	Pairs="{Binding MyPairs}"
	SelectedBorderColor="DodgerBlue"
	ShowAttempts="True"
	WrongColor="Crimson" />

Step 3: Customization via Bindable Properties

To make the component flexible, we exposed a wide range of bindable properties, including:

  • Colors: For correct, wrong, selected, and default states (both border and text).
  • Button: Text, style, command, and event handler.
  • Delay: Feedback display duration (in ms).
  • ShowAttempts: Whether to display the attempt count.
  • AttemptCountStringFormat: Custom format for the attempt label.
  • Pairs: The collection of pairs to match.

This allows consumers to fully tailor the component’s appearance and behavior.


Step 4: Selection and Matching Logic

The core logic ensures:

  • Only one item per column can be selected at a time.
  • When two items are selected, TryMatch is called.
  • The component disables both CollectionViews during feedback (using a private _isBusy flag and a helper method).
  • Visual feedback is shown for a configurable delay.
  • Matched pairs are disabled and colored appropriately.
  • The attempt count is incremented and can be displayed.
  • The “Continue” button is only enabled when all pairs are matched.

Example of the optimized TryMatch method:

private async void TryMatch()
{
    if (selectedLeft == null || selectedRight == null || _isBusy) return; _isBusy = true; SetCollectionsEnabled(false);
    AttemptCount++;

    var leftMatch = LeftWords.FirstOrDefault(x => x.Left == selectedLeft.Left);
    var rightMatch = RightWords.FirstOrDefault(x => x.Right == selectedRight.Right);

    if (leftMatch == null || rightMatch == null)
    {
        _isBusy = false;
        SetCollectionsEnabled(true);
        return;
    }

    if (selectedLeft.Right == selectedRight.Right)
    {
        // Correct match feedback
        leftMatch.LeftColor = CorrectColor;
        leftMatch.LeftTextColor = CorrectTextColor;
        leftMatch.IsMatched = true;
        leftMatch.IsLeftEnabled = false;

        rightMatch.RightColor = CorrectColor;
        rightMatch.RightTextColor = CorrectTextColor;
        rightMatch.IsMatched = true;
        rightMatch.IsRightEnabled = false;
    }
    else
    {
        // Wrong match feedback
        leftMatch.LeftColor = WrongColor;
        leftMatch.LeftTextColor = WrongTextColor;
        rightMatch.RightColor = WrongColor;
        rightMatch.RightTextColor = WrongTextColor;
    }

    await Task.Delay(Delay);

    if (selectedLeft.Right != selectedRight.Right)
    {
        // Reset colors after wrong match
        leftMatch.LeftColor = DefaultStrokeColor;
        leftMatch.LeftTextColor = DefaultTextColor;
        rightMatch.RightColor = DefaultStrokeColor;
        rightMatch.RightTextColor = DefaultTextColor;
    }

    selectedLeft = null;
    selectedRight = null;
    LeftCollection.SelectedItem = null;
    RightCollection.SelectedItem = null;

    UpdateAllMatched();

    if (!AllMatched)
        SetCollectionsEnabled(true);

    _isBusy = false;
}

Step 5: Button Actions

The “Continue” button supports both MVVM and code-behind patterns:

  • ButtonConfirmCommand: Bind to an ICommand in your ViewModel.
  • ButtonConfirmClicked: Attach an event handler in code-behind.

Both are triggered when the button is clicked.

Step 6: Accessibility and Usability

  • The component disables selection while feedback is being shown.
  • Once all pairs are matched, the button is enabled and the collections are disabled.
  • All UI text and colors are customizable for accessibility and localization.

Step 7: Usage in a Page

Here’s how you can use the component in your MAUI page:

<components:MatchingPairsView
	ButtonConfirmCommand="{Binding OnContinueCommand}"
	ButtonConfirmText="Next"
	CorrectColor="LimeGreen"
	DefaultBackgroundColor="White"
	DefaultStrokeColor="Gray"
	DefaultTextColor="Black"
	Delay="300"
	Pairs="{Binding MyPairs}"
	SelectedBorderColor="DodgerBlue"
	ShowAttempts="True"
	WrongColor="Crimson" />

Step 8: Full List of Parameters

Property NameTypeDefaultDescription
PairsObservableCollectionThe list of pairs to match.
ShowAttemptsbooltrueWhether to display the attempt count label.
AttemptCountint0The number of attempts made (read-only, updated by the component).
AllMatchedboolfalseIndicates if all pairs have been matched (read-only, updated by the component).
Delayint200Delay in milliseconds for feedback display after a match attempt.
ButtonConfirmTextstring"Continue"The text displayed on the confirm button.
ButtonConfirmStyleStylenullThe style applied to the confirm button.
ButtonConfirmCommandICommandnullCommand executed when the confirm button is clicked.
ButtonConfirmClickedEventHandlernullEvent raised when the confirm button is clicked.
CorrectColorColorGreenBorder color for a correct match.
CorrectTextColorColorGreenText color for a correct match.
WrongColorColorRedBorder color for an incorrect match.
WrongTextColorColorRedText color for an incorrect match.
SelectedBorderColorColorDodgerBlueBorder color for a selected item.
SelectedTextColorColorDodgerBlueText color for a selected item.
DefaultStrokeColorColor#FF444444Default border color for items.
DefaultBackgroundColorColorWhiteDefault background color for items.
DefaultTextColorColorWhiteDefault text color for items.

Conclusion

Through iterative improvements and a focus on flexibility, we’ve built a robust Matching Pairs component for .NET MAUI. It’s easy to use, highly customizable, and ready for integration into your next educational or gamified app.

Happy coding!

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.